package com.feature50.makeover;

import com.feature50.makeover.ui.CandylandScrollbarUI;
import com.feature50.makeover.utils.FontResolver;
import com.feature50.makeover.utils.ImageUtils;
import com.feature50.makeover.utils.PaintUtils;
import com.feature50.makeover.utils.Platform;
import com.jgoodies.forms.factories.Borders;
import com.jgoodies.forms.layout.CellConstraints;
import com.jgoodies.forms.layout.FormLayout;
import org.apache.log4j.Logger;

import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.DefaultListModel;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JTree;
import javax.swing.KeyStroke;
import javax.swing.ListCellRenderer;
import javax.swing.SwingUtilities;
import javax.swing.plaf.basic.BasicTreeUI;
import javax.swing.text.JTextComponent;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import java.awt.CardLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class NavigationTopPanel extends JPanel {
    private static final Logger logger = Logger.getLogger(NavigationTopPanel.class);

    private static CellConstraints CC = new CellConstraints();
    private JTextComponent text;
    private JTree tree;
    private DesktopFrame frame;
    private NavigationPanel navPanel;

    private boolean searchMode = false;
    private JPanel mainPanel;
    private TreeModel model;
    private JList searchResults;
    private NavMatchThread searchThread;

    private String lastSearchText;
    private static final int TREE_POINT_WIDTH = 8;
    private static final int TREE_ENDSPACE = TREE_POINT_WIDTH + 5;

    public NavigationTopPanel(DesktopFrame frame, NavigationPanel panel) {
        this.frame = frame;
        navPanel = panel;
        init();
    }

    private void init() {
        FormLayout layout = new FormLayout("fill:default:grow", "default, fill:default:grow");
        setLayout(layout);

        JPanel findPanel = createFindPanel();

        add(findPanel, CC.xy(1, 1));

        mainPanel = createMainPanel();

        add(mainPanel, CC.xy(1, 2));

        // the quick find search is disabled until the tree is populated and ready
        text.setEnabled(false);

        // make tree navigation work even when the quick find text box has focus
        text.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), "up");
        text.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), "down");
        text.getActionMap().put("up", new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                moveSelection(true);
            }
        });
        text.getActionMap().put("down", new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                moveSelection(false);
            }
        });

        // make tree folder expansion contraction work
        text.addKeyListener(new KeyAdapter() {
            public void keyTyped(KeyEvent e) {
                if (!searchMode) {
                    if ((e.getKeyChar() == '+') || (e.getKeyChar() == '=')) {
                        setSelectionExpanded(true);
                        e.consume();
                    } else if (e.getKeyChar() == '-') {
                        setSelectionExpanded(false);
                        e.consume();
                    }
                }
            }
        });

        text.addKeyListener(new KeyAdapter() {
            public void keyReleased(KeyEvent e) {
                String t = text.getText().trim().toLowerCase();
                if (!t.isEmpty()) {
                    searchMode = true;

                    if ((lastSearchText != null) && (lastSearchText.equals(t))) return;
                    lastSearchText = t;

                    if (searchThread != null) searchThread.setCancelled(true);
                    searchThread = new NavMatchThread(model, searchResults, mainPanel);
                    searchThread.setText(t);
                    searchThread.start();
                } else {
                    if (searchMode) {
                        searchMode = false;
                        lastSearchText = null;
                        CardLayout layout = (CardLayout) mainPanel.getLayout();
                        layout.show(mainPanel, "tree");
                    }
                }
            }
        });

        // enter key launches currently selected app
        KeyListener enterLauncher = new KeyAdapter() {
            public void keyPressed(KeyEvent e) {
                if (e.getKeyCode() == KeyEvent.VK_ENTER) {
                    Object node = null;
                    if (searchMode) {
                        node = searchResults.getSelectedValue();
                    } else {
                        TreePath path = tree.getSelectionPath();
                        if (path != null) node = path.getLastPathComponent();
                    }
                    if (node != null) frame.launchNavApp((ENode) node);
                }
            }
        };
        tree.addKeyListener(enterLauncher);
        text.addKeyListener(enterLauncher);
        searchResults.addKeyListener(enterLauncher);

        // Number Launcher (tm) ;-)
        KeyListener numberLauncher = new KeyAdapter() {
            public void keyTyped(KeyEvent e) {
                char c = e.getKeyChar();
                if (Character.isDigit(c)) {
                    e.consume();

                    if (searchMode) {
                        int number = Integer.parseInt(String.valueOf(c));
                        if (searchResults.getModel().getSize() >= number) {
                            searchResults.setSelectedIndex(number - 1);
                            final Object node = searchResults.getSelectedValue();

                            // enqueue later to give the ui a chance to display the selection change
                            SwingUtilities.invokeLater(new Runnable() {
                                public void run() {
                                    frame.showApplication((ENode) node);
                                }
                            });
                        }
                    }
                }
            }
        };
        text.addKeyListener(numberLauncher);
    }

    private void setSelectionExpanded(boolean expanded) {
        int[] rows = tree.getSelectionRows();
        if ((rows == null) || (rows.length == 0)) return;
        int row = rows[0];
        if (row == -1) return;
        if (expanded) { tree.expandRow(row); } else { tree.collapseRow(row); }
    }

    private void moveSelection(boolean up) {
        if (!searchMode) {
            if (tree.getRowCount() == 0) return;

            int[] rows = tree.getSelectionRows();
            int newrow;
            if ((rows == null) || (rows.length == 0)) {
                newrow = 0;
            } else {
                newrow = rows[0] + ((up) ? -1 : 1);
            }
            if (newrow > (tree.getRowCount() - 1)) newrow = 0;
            if (newrow < 0) newrow = tree.getRowCount() - 1;
            tree.setSelectionRow(newrow);
            tree.scrollRowToVisible(newrow);
        } else {
            if (searchResults.getModel().getSize() == 0) return;

            int index = searchResults.getSelectedIndex();
            if (index != -1) index += (up) ? -1 : 1;
            if (index == -1) {
                index = searchResults.getModel().getSize() - 1;
            } else if (index > searchResults.getModel().getSize() - 1) {
                index = 0;
            }

            searchResults.setSelectedIndex(index);
            searchResults.ensureIndexIsVisible(index);
        }
    }

    private JPanel createMainPanel() {
        JPanel mainPanel = new JPanel();
        mainPanel.setOpaque(false);
        mainPanel.setLayout(new CardLayout());

        JPanel treePanel = createTreePanel();
        mainPanel.add(treePanel, "tree");

        JPanel searchPanel = createSearchPanel();
        mainPanel.add(searchPanel, "search");

        return mainPanel;
    }

    private JPanel createSearchPanel() {
        JPanel searchPanel = new JPanel();
        searchPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
        searchPanel.setOpaque(false);

        FormLayout layout = new FormLayout("fill:default:grow", "default, 5px, fill:default:grow");
        searchPanel.setLayout(layout);

        JLabel label = new JLabel("Search Results");
        label.setFont(FontResolver.getFont(NavigationPanel.FONT_NAV_HEADER));
        label.setForeground(Color.WHITE);
        searchPanel.add(label, CC.xy(1, 1));

        searchResults = new JList();
        searchResults.setOpaque(false);
        JScrollPane pane = new JScrollPane(searchResults);
        pane.setBorder(null);
        pane.setOpaque(false);
        pane.getViewport().setOpaque(false);
        pane.getVerticalScrollBar().setUI(new CandylandScrollbarUI());
        pane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);

        searchPanel.add(pane, CC.xy(1, 3));

        searchResults.setCellRenderer(new SearchCellRenderer());

        searchResults.addMouseListener(new MouseAdapter() {
            private Object selectedObject;

            public void mouseClicked(MouseEvent e) {
                Object o = searchResults.getSelectedValue();
                if ((e.getClickCount() == 2) && (o == selectedObject)) {
                    frame.launchNavApp((ENode) o);
                }
                selectedObject = o;
            }
        });

        NavigationBottomPanel.addHoverNodeListeners(navPanel, searchResults);

        return searchPanel;
    }

    private int getTreeTotalWidth() {
        return tree.getWidth() - TREE_ENDSPACE;
    }

    private int getTreeStarX() {
        return getTreeTotalWidth() - NavigationPanel.STAR_NORMAL.getWidth();
    }

    private int getTreeStarWidth() {
        return NavigationPanel.STAR_NORMAL.getWidth();
    }

    private JPanel createTreePanel() {
        JPanel treePanel = new JPanel();
        treePanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
        treePanel.setOpaque(false);

        FormLayout layout = new FormLayout("fill:default:grow", "default, 5px, fill:default:grow");
        treePanel.setLayout(layout);

        JLabel label = new JLabel("All Applications");
        label.setFont(FontResolver.getFont(NavigationPanel.FONT_NAV_DEFAULT));

        label.setForeground(Color.WHITE);
        treePanel.add(label, CC.xy(1, 1));

        tree = new JTree() {
            protected void paintComponent(Graphics g) {
                Graphics2D g2d = (Graphics2D) g;

                int totalWidth = getTreeTotalWidth();

                int starx = getTreeStarX();
                int stary;

                g2d.setColor(new Color(255, 255, 255, 32));
                for (int i = 0; i < getRowCount(); i++) {
                    Rectangle rect = getRowBounds(i);
                    if (i % 2 == 0) g2d.fillRect(1, rect.y, getWidth() - 5, rect.height);

                    Object row = getPathForRow(i).getLastPathComponent();
                    if (model.isLeaf(row)) {
                        ENode node = (ENode) row;

                        stary = rect.y + (rect.height / 2) - (NavigationPanel.STAR_NORMAL.getHeight() / 2);

                        boolean mouseOver = isHoverNode(node, tree);
                        if (navPanel.isFavorite(node)) {
                            g2d.drawImage(NavigationPanel.STAR_MARKED, starx, stary, null);
                        } else if (mouseOver) {
                            g2d.drawImage(NavigationPanel.STAR_NORMAL, starx, stary, null);
                        }
                    }
                }

                g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

                int[] rows = getSelectionRows();
                int pointWidth = 8;
                int endspace = pointWidth + 5;
                if (rows != null) {
                    for (int i = 0; i < rows.length; i++) {
                        for (int z = 0; z < 3; z++) {
                            if (z == 0) {
                                g2d.setColor(new Color(0, 0, 0, 64));
                                g2d.translate(2, 2);
                            } else if (z == 1) {
                                g2d.setColor(new Color(0, 0, 0, 128));
                                g2d.translate(-1, -1);
                            } else {
                                g2d.setColor(new Color(202, 202, 202));
                                g2d.translate(-1, -1);
                            }
                            Rectangle rect = getRowBounds(rows[i]);
                            g2d.drawLine(0, rect.y + 1, 0, rect.y + rect.height - 2);
                            g2d.fillRect(1, rect.y, totalWidth, rect.height);
                            g2d.fillPolygon(new int[] { totalWidth, totalWidth + TREE_POINT_WIDTH + 2, totalWidth }, new int[] { rect.y, rect.y + (rect.height / 2), rect.y + rect.height }, 3);

                            Object row = getPathForRow(rows[i]).getLastPathComponent();
                            if (model.isLeaf(row)) {
                                ENode node = (ENode) row;

                                stary = rect.y + (rect.height / 2) - (NavigationPanel.STAR_NORMAL.getHeight() / 2);

                                boolean mouseOver = isHoverNode(node, tree);
                                if (navPanel.isFavorite(node)) {
                                    g2d.drawImage(NavigationPanel.STAR_MARKED, starx, stary, null);
                                } else if (mouseOver) {
                                    g2d.drawImage(NavigationPanel.STAR_SELECTED, starx, stary, null);
                                }
                            }
                        }
                    }
                }

                Platform.setAntialiasing(g, tree.getFont());
                super.paintComponent(g);
            }
        };
        //tree.setFocusable(false);
        tree.setFont(FontResolver.getFont(NavigationPanel.FONT_NAV_DEFAULT));
        tree.setOpaque(false);
        tree.setRootVisible(false);
        tree.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
        tree.setRowHeight(19);

        tree.addMouseListener(new MouseAdapter() {
            int oldSelection = -1;

            public void mousePressed(MouseEvent e) {
                int[] ints = tree.getSelectionRows();
                if ((ints != null) && (ints.length == 1)) {
                    oldSelection = ints[0];
                }

                int row = tree.getRowForLocation(e.getPoint().x, e.getPoint().y);
                if (row == -1) {
                    row = tree.getClosestRowForLocation(e.getPoint().x, e.getPoint().y);
                    tree.setSelectionRow(row);
                }
            }

            public void mouseClicked(MouseEvent e) {
                int[] rows = tree.getSelectionRows();
                int row = ((rows != null) && (rows.length == 1)) ? rows[0] : -1;

                if (row != -1) {
                    if ((oldSelection == row) && (e.getClickCount() == 2)) {
                        showSelectedNode();
                    }
                }
            }
        });

        // track mouse movements related to the favorites feature
        tree.addMouseMotionListener(new MouseAdapter() {
            public void mouseMoved(MouseEvent e) {
                if (navPanel.getBuilt() != 1f) return;

                Object node = null;

                int row = tree.getRowForLocation(e.getX(), e.getY());
                if (row == -1) row = tree.getClosestRowForLocation(e.getX(), e.getY());
                if (row != -1) node = tree.getPathForRow(row).getLastPathComponent();

                Object oldNode = tree.getClientProperty("hoverNode");
                if (oldNode != node) {
                    tree.putClientProperty("hoverNode", node);
                    tree.repaint();
                }
            }
        });

        // track mouse movements related to the favorites feature
        tree.addMouseListener(new MouseAdapter() {
            public void mouseClicked(MouseEvent e) {
                int starX = getTreeStarX();
                int width = getTreeStarWidth();
                if ((e.getX() >= starX) && (e.getX() <= starX + width)) {
                    int row = tree.getRowForLocation(e.getX(), e.getY());
                    if (row == -1) row = tree.getClosestRowForLocation(e.getX(), e.getY());
                    if (row != -1) {
                        Object node = tree.getPathForRow(row).getLastPathComponent();
                        if (model.isLeaf(node)) {
                            ENode n = (ENode) node;
                            if (navPanel.isFavorite(n)) {
                                navPanel.removeFavorite(n);
                            } else {
                                navPanel.addFavorite(n);
                            }
                            tree.repaint();
                        }
                    }
                }
            }

            public void mouseExited(MouseEvent e) {
                Object oldNode = tree.getClientProperty("hoverNode");
                if (oldNode != null) {
                    tree.putClientProperty("hoverNode", null);
                    tree.repaint();
                }
            }
        });

        tree.putClientProperty("JTree.lineStyle", "None");
        BasicTreeUI ui = (BasicTreeUI) tree.getUI();
        ui.setCollapsedIcon(null);
        ui.setExpandedIcon(null);
        tree.setCellRenderer(new TreeCellRenderer() {
            private JLabel label;
            private Icon folder;

            public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
                if (label == null) {
                    label = new JLabel();
                    label.setFont(tree.getFont());
                    folder = ImageUtils.getIcon("icn_folder.png");
                }

                Icon icon;
                if (leaf) {
                    icon = DesktopFrame.ICON_APP_DEFAULT;
                } else {
                    icon = folder;
                }

                label.setText(value.toString());
                label.setIcon(icon);
                label.setForeground((selected) ? Color.BLACK : Color.WHITE);

                return label;
            }
        });
        tree.setModel(getTreeModel());
        for (int i = 0; i < tree.getRowCount(); i++) tree.expandRow(i);

        JScrollPane pane = new JScrollPane(tree);
        pane.setBorder(null);
        pane.setOpaque(false);
        pane.getViewport().setOpaque(false);
        pane.getVerticalScrollBar().setUI(new CandylandScrollbarUI());
        pane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        treePanel.add(pane, CC.xy(1, 3));

        return treePanel;
    }

    protected void paintComponent(Graphics g) {
        PaintUtils.paintGradient(
                (Graphics2D) g,
                new Point2D.Double(0, 0),
                new Point2D.Double(0, 1),
                new Point(0, 0),
                new Dimension(getWidth(), getHeight()),
                new Color[] { Color.BLACK, new Color(33, 35, 42) },
                null
        );
    }

    private boolean isHoverNode(ENode node, JComponent component) {
        Object n = component.getClientProperty("hoverNode");
        return n == node;
    }

    private JPanel createFindPanel() {
        JPanel findPanel = new JPanel() {
            protected void paintComponent(Graphics g) {
                PaintUtils.paintCandylandBackground(g, getHeight(), getWidth(), false);
            }
        };
        findPanel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));

        FormLayout layout = new FormLayout("default, 10px, fill:default:grow", "default");
        findPanel.setLayout(layout);

        JLabel label = new JLabel("Quick Find");
        label.setBorder(BorderFactory.createEmptyBorder(2, 0, 0, 0));
        label.setFont(FontResolver.getFont(NavigationPanel.FONT_NAV_HEADER));
        label.setForeground(Color.WHITE);
        findPanel.add(label, CC.xy(1, 1));

        text = new JTextField();
        findPanel.add(text, CC.xy(3, 1));

        return findPanel;
    }

    private TreeModel getTreeModel() {
        DefaultMutableTreeNode root = new DefaultMutableTreeNode("Applications");
        return new DefaultTreeModel(root);
    }

    private void showSelectedNode() {
        int[] rows = tree.getSelectionRows();
        if (rows == null) return;
        if (rows.length > 1) return;

        frame.launchNavApp((ENode) tree.getPathForRow(rows[0]).getLastPathComponent());
    }

    public JTextComponent getText() {
        return text;
    }

    public void setTreeModel(TreeModel model) {
        this.model = model;
        tree.setModel(model);
        for (int i = 0; i < tree.getRowCount(); i++) tree.expandRow(i);

        // just in case we're doing an animation when the tree model is set, set any cached images to null so the tree is rebuilt
        navPanel.resetImages();

        // enable the quick find text box
        text.setEnabled(true);
        text.requestFocus();
    }

    public JTree getTree() {
        return tree;
    }

    private class SearchCellRenderer implements ListCellRenderer {
        // this maps is for performance; it may not be necessary but I can't help myself
        private Map<ENode, String> nodePathMap = new HashMap<ENode, String>();

        private JComponent cellPane;
        private CircleLabel numberLabel;
        private JLabel iconLabel;
        private JLabel nameLabel;
        private JLabel pathLabel;
        private BufferedImage starImage;

        public SearchCellRenderer() {
            cellPane = new JPanel() {
                protected void paintComponent(Graphics g) {
                    super.paintComponent(g);

                    if (starImage == null) return;

                    int starx = getWidth() - starImage.getWidth() - 5;
                    int stary = (getHeight() / 2) - (starImage.getHeight() / 2);

                    Graphics2D g2d = (Graphics2D) g.create();
                    g2d.drawImage(starImage, starx, stary, null);
                    g2d.dispose();
                }
            };
            cellPane.setBorder(Borders.DLU2_BORDER);

            FormLayout layout = new FormLayout("19px, 5px, default, 7px, fill:default:grow", "default, default");
            cellPane.setLayout(layout);

            numberLabel = new CircleLabel();
            numberLabel.setFont(new Font("Arial", Font.BOLD, 14));
            numberLabel.setBackground(Color.BLACK);
            numberLabel.setBorderColor(Color.WHITE);
            cellPane.add(numberLabel, CC.xy(1, 1));

            iconLabel = new JLabel();
            cellPane.add(iconLabel, CC.xy(3, 1));

            nameLabel = new JLabel();
            nameLabel.setFont(new Font("Franklin Gothic Medium", Font.PLAIN, 18));
            cellPane.add(nameLabel, CC.xy(5, 1));

            pathLabel = new JLabel();
            pathLabel.setFont(new Font("Franklin Gothic Medium", Font.PLAIN, 11));
            cellPane.add(pathLabel, CC.xy(5, 2));
        }

        public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
            ENode node = (ENode) value;

            numberLabel.setText(String.valueOf(index + 1));
            numberLabel.setVisible(index <= 8);
            iconLabel.setIcon(frame.getNodeIcon(node));
            nameLabel.setText(node.getDisplay());

            if (isSelected) {
                cellPane.setBackground(new Color(0, 0, 255, 128));
                cellPane.setOpaque(true);
                nameLabel.setForeground(Color.WHITE);
                pathLabel.setForeground(Color.WHITE);
            } else {
                cellPane.setBackground(new Color(255, 255, 255, 32));
                cellPane.setOpaque(index % 2 == 0);
                nameLabel.setForeground(Color.WHITE);
                pathLabel.setForeground(Color.WHITE);
            }

            String path = nodePathMap.get(node);
            if (path == null) {
                StringBuilder builder = new StringBuilder();

                ENode parent = node.getParent();
                while (parent != null) {
                    if (builder.length() != 0) {
                        builder.insert(0, " > ");
                    }
                    builder.insert(0, parent.getDisplay());
                    parent = parent.getParent();
                }

                path = builder.toString();
                nodePathMap.put(node, path);
            }

            pathLabel.setText(path);

            if (model.isLeaf(node)) {
                if (navPanel.isFavorite((ENode) node)) {
                    starImage = NavigationPanel.STAR_MARKED;
                } else if (isHoverNode(node, list)) {
                    // the selection color for the results list is different than for other components; it works with the normal star just fine
                    //starImage = (isSelected) ? NavigationPanel.STAR_SELECTED : NavigationPanel.STAR_NORMAL;
                    starImage = NavigationPanel.STAR_NORMAL;
                } else {
                    starImage = null;
                }
            } else {
                starImage = null;
            }

            return cellPane;
        }
    }

    private static class NavMatchThread extends Thread {
        private boolean cancelled;
        private static List<ENode> nodes;
        private String text;
        private TreeModel model;
        private JList resultList;
        private JPanel mainPanel;

        public NavMatchThread(TreeModel model, JList resultList, JPanel mainPanel) {
            this.model = model;
            this.resultList = resultList;
            this.mainPanel = mainPanel;
        }

        public String getText() {
            return text;
        }

        public void setText(String text) {
            this.text = text;
        }

        public boolean isCancelled() {
            return cancelled;
        }

        public void setCancelled(boolean cancelled) {
            this.cancelled = cancelled;
        }

        public void run() {
            // avoid race conditions
            final String t = getText().toLowerCase();

            synchronized (NavMatchThread.class) {
                if (nodes == null) nodes = getAllLeafNodes(model);
            }

            List<NavMatchScore> scores = new ArrayList<NavMatchScore>();

            for (int i = 0; i < nodes.size(); i++) {
                if (isCancelled()) break;

                ENode node = nodes.get(i);

                String display = node.getDisplay().toLowerCase();
                int score = -1;

                // apply the matching rules
                if (firstWordMatchesSearchString(display, t)) score = 5;
                if ((score == -1) && (display.startsWith(t))) score = 4;
                if ((score == -1) && (wordMatchesSearchString(display, t))) score = 3;
                if ((score == -1) && (display.indexOf(text) != -1)) score = 2;

                if (score != -1) scores.add(new NavMatchScore(node, score));
            }

            Collections.sort(scores);

            final DefaultListModel model = new DefaultListModel();
            for (int i = 0; i < scores.size(); i++) {
                NavMatchScore score = scores.get(i);
                model.addElement(score.getNode());
            }

            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    if (isCancelled()) return;

                    resultList.setModel(model);
                    if (model.getSize() > 0) resultList.setSelectedIndex(0);

                    CardLayout layout = (CardLayout) mainPanel.getLayout();
                    layout.show(mainPanel, "search");
                }
            });
        }

        private List<ENode> getAllLeafNodes(TreeModel model) {
            List<ENode> leaves = new ArrayList<ENode>();
            getLeafNodes(model.getRoot(), model, leaves);
            return leaves;
        }

        private void getLeafNodes(Object node, TreeModel model, List leaves) {
            if (model.isLeaf(node)) {
                leaves.add(node);
            } else {
                for (int i = 0; i < model.getChildCount(node); i++) {
                    Object child = model.getChild(node, i);
                    getLeafNodes(child, model, leaves);
                }
            }
        }

        // creating a bunch of mini-methods to attempt readability

        private boolean wordMatchesSearchString(String display, String t) {
            String[] words = display.split(" ");
            for (int i = 0; i < words.length; i++) {
                String word = words[i];
                if (word.equals(t)) return true;
            }
            return false;
        }

        private boolean firstWordMatchesSearchString(String display, String t) {
            if (!(display.startsWith(t))) return false;
            if (display.length() == t.length()) return true;
            if (display.charAt(t.length()) == ' ') return true;
            return false;
        }
    }

    private static class NavMatchScore implements Comparable {
        private ENode node;

        /*
         * SCORES
         * - first word is exact match of text: 5
         * - starts with text, not exact word match: 4
         * - a word matches text exactly: 3
         * - contains the text string: 2
         * - previous matched current node to text string: UNSUPPORTED
         */
        private int score;

        public NavMatchScore(ENode node, int score) {
            this.node = node;
            this.score = score;
        }

        public ENode getNode() {
            return node;
        }

        public int getScore() {
            return score;
        }

        public int compareTo(Object o) {
            NavMatchScore score = (NavMatchScore) o;
            if (this.score != score.score) return score.score - this.score;
            return node.getDisplay().compareTo(score.getNode().getDisplay());
        }
    }
}